import {
  BackSide,
  BoxGeometry,
  CircleGeometry,
  Color,
  DirectionalLight,
  DoubleSide,
  FrontSide,
  Group,
  Mesh,
  MeshBasicMaterial,
  MeshPhongMaterial,
  MeshStandardMaterial,
  PlaneGeometry,
  ShaderMaterial,
  Vector2,
  Vector3,
} from "three";

import NPC from "../Entitys/NPC";
import Troop from "./Troop";

import { RandomFromInterval } from "../Utils/Util";
import { OrbitControls } from "../Libs/OrbitControls";
import Entity from "./Entity";

export type Area = { [x: string]: { [x: string]: AreaItem } } & { count: number };
export type AreaItem = { type: string; value: NPC };

export default class World {
  direcLight: DirectionalLight;
  control: OrbitControls;
  areaSize = 16;
  visualScope: { short: number; long: number };
  cameraMagnify = 1;

  generateScope = { max: 3, min: 1 };
  npcConfigMap = {
    Number1: {
      modelUrl: `assest/model/Number-1.glb`,
      scale: 0.1,
      scope: this.generateScope,
      mater: {
        emissiveIntensity: 0,
        emissiveMap: null,
        emissive: new Color("black"),
        metalness: 0.2,
        roughness: 0.2,
        color: (mater: MeshStandardMaterial) => {
          return mater.color && !mater.color.getHex() ? new Color("white") : mater.color;
        },
      },
      // animas: { scan: { i: 0 } },
      angle: 0,
      collision: true,
    },
  };

  originCameraPos = new Vector3(0, 3, 4);
  originLightPos = new Vector3(0, 5, 3.5);
  constructor() {
    render.mainCamera.position.copy(this.originCameraPos);
    render.mainCamera.lookAt(new Vector3());

    this.BuildWorld();
  }

  originAreaGroup: Group;
  areaMatrix = [[[[]]]];

  async BuildWorld() {
    this.LoadMap();
    await this.LoadTroop();
    this.UpdateVisualScope();
    await this.LoadLight();

    render.addClockEvent("LightAndCameraFollowPlayer", this.LightAndCameraFollowPlayer.bind(this));
    render.addClockEvent("worldClock", this.WorldClock.bind(this));

    let size = 10;
    let plane = new PlaneGeometry(size, size);
    let m2 = new ShaderMaterial({
      side: DoubleSide,
      transparent: true,
      uniforms: {
        color1: { value: new Color("white") },
        color2: { value: new Color("#CCCCCC") },
        size: { value: [window.innerWidth, window.innerHeight] },
        num: { value: 800 },
        ratio: { value: window.devicePixelRatio },
      },
      vertexShader: `
        void main() {
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
        }`,
      fragmentShader: `
          uniform vec3 color1;
          uniform vec3 color2;
          uniform vec2 size;
          uniform float num;
          uniform float ratio;

          void main() {
            float d = distance(size * ratio * 0.5, gl_FragCoord.xy) / num;
            d = smoothstep(0., 1., d);

            gl_FragColor  = mix(vec4(color1, 0.), vec4(color2, 1.), d);
          }`,
    });

    let mesh = new Mesh(plane, m2);
    mesh.renderOrder = 2;
    mesh.position.z -= 1;
    render.mainCamera.add(mesh);
    render.mainScene.add(render.mainCamera);
  }

  mesh = new Mesh(new BoxGeometry(1, 1), new MeshBasicMaterial({ color: new Color("#EC6633") }));

  oldS: number;
  WorldClock(t: number) {
    let s = Math.round(t / 500);
    if (s != this.oldS) {
      this.GenerationNpc();
      this.oldS = s;
    }
  }

  UpdateVisualScope() {
    let { y, z } = render.mainCamera.position;

    let vertical = Math.atan(z / y) - Math.PI / 8;
    let offset = y * Math.tan(vertical);
    let short = z - offset;
    let long = y * Math.tan(Math.atan(z / y) + Math.PI / 8) - z;

    this.visualScope = { short, long };
  }

  LoadCamera() {
    this.control = new OrbitControls(render.mainCamera, render.r.domElement);
    // this.control.enableZoom = false;
    // this.control.enableRotate = false;
    render.addClockEvent("update", () => {
      this.control.update();
    });
  }

  async LoadLight() {
    this.direcLight = new DirectionalLight("#fff", 1);
    this.direcLight.position.copy(this.originLightPos);

    this.direcLight.castShadow = true;
    this.direcLight.shadow.camera.near = 0.5;
    this.direcLight.shadow.camera.far = 10;
    let { long, short } = world.visualScope;
    let w = (long + short) * render.mainCamera.aspect;

    this.direcLight.shadow.camera.top = -long;
    this.direcLight.shadow.camera.bottom = short;
    this.direcLight.shadow.camera.left = -w;
    this.direcLight.shadow.camera.right = w;
    this.direcLight.shadow.bias = 0.00001;

    render.mainScene.add(this.direcLight, this.direcLight.target);

    // await toAsync(this.direcLight, {
    //   duration: 1,
    //   ease: "back.out(2)",
    //   intensity: 1.5,
    //   angle: MathUtils.degToRad(7),
    // });
  }

  LightAndCameraFollowPlayer() {
    let { x, z } = this.troop.group.position;
    let soy = this.originLightPos.y;
    let soz = this.originLightPos.z;
    let coy = this.originCameraPos.y;
    let coz = this.originCameraPos.z;

    this.direcLight.position.set(x, soy * (this.cameraMagnify + coy / soy / coy), z + soz * this.cameraMagnify);
    this.direcLight.target.position.set(x, 0, z);
    render.mainCamera.position.set(x, coy * this.cameraMagnify, z + coz * this.cameraMagnify);
  }

  LoadMap() {
    let size = 1000;
    let plane = new PlaneGeometry(size, size);
    let mater = new MeshPhongMaterial({
      color: new Color("white"),
      side: BackSide,
      transparent: true,
    });

    let mesh = new Mesh(plane, mater);
    mesh.receiveShadow = true;
    mesh.renderOrder = 1;
    mesh.rotation.x += Math.PI / 2;

    let circle = new Mesh(
      new CircleGeometry(0.5),
      new MeshPhongMaterial({
        transparent: true,
        opacity: 0,
        side: BackSide,
      })
    );
    circle.rotation.x += Math.PI / 2;
    circle.position.y += 0.01;
    render.addClockEvent("circle_zoom", (t) => {
      let n = +((Math.sin(t * 0.005) + 1) / 2).toFixed(2);
      circle.scale.setScalar(n);
    });

    render.mainScene.add(circle);
    render.mainScene.add(mesh);
  }

  GetIndexAndPlusNumber(v2: Vector2) {
    let i = Math.sign(v2.x) == -1 ? 1 : 0;
    let j = Math.sign(v2.y) == -1 ? 1 : 0;
    return { i, j, x: Math.abs(v2.x), y: Math.abs(v2.y) };
  }

  SetItem(v3: Vector3, entity: AreaItem) {
    let v = new Vector2(Math.trunc(v3.x), Math.trunc(v3.z));
    let pos = [];
    {
      let { i, j, x, y } = this.GetIndexAndPlusNumber(v);
      this.areaMatrix[x] = this.areaMatrix[x] || [];
      this.areaMatrix[x][i] = this.areaMatrix[x][i] || [];
      this.areaMatrix[x][i][y] = this.areaMatrix[x][i][y] || [];
      this.areaMatrix[x][i][y][j] = this.areaMatrix[x][i][y][j] || {};
      this.areaMatrix[x][i][y][j][entity.value.id] = entity;
      pos.push(...[x, i, y, j]);
      entity.value.matrixCoord = pos;
    }
    this.Items = [];
    this.GetAllAreaItem(this.Items);
  }

  DelItem(entity: Entity) {
    let matrix = this.areaMatrix as any;
    for (let i = 0; i < entity.matrixCoord.length; i++) {
      matrix = matrix[entity.matrixCoord[i]];
    }
    delete matrix[entity.id];
    this.Items = [];
    this.GetAllAreaItem(this.Items);
  }

  Items: AreaItem[] = [];
  GetAllAreaItem(items: AreaItem[], matrix = this.areaMatrix) {
    if (matrix.length != undefined) {
      for (let i = 0; i < matrix.length; i++) {
        if (matrix[i]) this.GetAllAreaItem(items, matrix[i]);
      }
    } else {
      items.push(...(Object.values(matrix) as any));
    }
  }

  troop: Troop;
  async LoadTroop() {
    this.troop = new Troop({
      modelUrl: `assest/model/Player.glb`,
      scale: 0.0003,
      angle: Math.PI,
      mater: { color: new Color("#67ABB8") },
    });
    await this.troop.AddPerson();
    this.troop.group.position.y -= 0.1;
    render.mainScene.add(this.troop.group);

    let oldS = 0;
    render.addClockEvent("addPerson", async (t) => {
      let s = Math.trunc(t / 1000 / 3);
      if (s != oldS) {
        if (this.troop.sum < 20) await this.troop.AddPerson();
        oldS = s;
      }
    });
  }

  async GenerationNpc() {
    if (this.Items.length > 50) return;
    let npcKeys = Object.keys(this.npcConfigMap);
    let n = Math.floor(RandomFromInterval(0, npcKeys.length));
    let key = npcKeys[n];

    let npc = new NPC({ type: key, ...this.npcConfigMap[key] });
    await npc.Builder();
    render.mainScene.add(npc.model);
  }
}
